#include "Subsystems/BlockBehaviors/SubsystemFluidBlockBehavior.h"
#include "Subsystems/SubsystemTerrain.h"
#include "Blocks/FluidBlock.h"
#include "Blocks/WaterBlock.h"
#include "Blocks/MagmaBlock.h"

#include "Managers/BlocksManager.h"
#include "Log.h"

namespace PocketSurvival
{
    bool SubsystemFluidBlockBehavior::isTheSameFluid(int32_t contents)
    {
        if (m_fluidType == FluidType::Water)
        {
            return GameSingleton::blocksManager->blocks[contents]->isBlock<WaterBlock *>();
        }
        else
        {
            return GameSingleton::blocksManager->blocks[contents]->isBlock<MagmaBlock *>();
        }
        return false;
    }

    void SubsystemFluidBlockBehavior::updateIsTop(int32_t value, int32_t x, int32_t y, int32_t z)
    {
        if (y < 255)
        {
            TerrainChunk *chunkAtCell = subsystemTerrain->getTerrainChunk(x >> 4, z >> 4);
            if (chunkAtCell != nullptr)
            {
                int32_t num = TerrainChunk::CalculateCellIndex(x & 0xF, y, z & 0xF);
                int32_t contents = Terrain::ExtractContents(chunkAtCell->getCellValueFast(num + 1));
                int32_t data = Terrain::ExtractData(value);
                bool isTop = (false == isTheSameFluid(contents));
                subsystemTerrain->changeOldCell(Point3(x, y, z), value, Terrain::ReplaceData(value, FluidBlock::SetIsTop(data, isTop)));
            }
        }
    }

    void SubsystemFluidBlockBehavior::onBlockAdded(int32_t value, int32_t oldValue, int32_t x, int32_t y, int32_t z)
    {
        updateIsTop(value, x, y, z);
    }

    void SubsystemFluidBlockBehavior::onBlockModified(int32_t value, int32_t oldValue, int32_t x, int32_t y, int32_t z)
    {
        updateIsTop(value, x, y, z);
    }

    void SubsystemFluidBlockBehavior::onNeighborBlockChanged(int32_t x, int32_t y, int32_t z, int32_t neighborX, int32_t neighborY, int32_t neighborZ)
    {
        m_toUpdate.emplace(x, y, z);
        if (neighborY == y + 1)
        {
            updateIsTop(subsystemTerrain->getCellValue(x, y, z), x, y, z);
        }
    }

    void SubsystemFluidBlockBehavior::onItemHarvested(int32_t x, int32_t y, int32_t z, int32_t blockValue, BlockDropValue &outDropValue, int32_t &outNewBlockValue)
    {
        outNewBlockValue = Terrain::MakeBlockValue(m_fluidBlock->getIndex());
        outDropValue.value = 0;
        outDropValue.count = 0;
    }

    std::shared_ptr<float> SubsystemFluidBlockBehavior::getSurfaceHeight(int32_t x, int32_t y, int32_t z, FluidBlock *&outSurfaceFluidBlock)
    {
        if (y >= 0 && y < 255)
        {
            TerrainChunk *chunkAtCell = subsystemTerrain->getTerrainChunk(x >> 4, z >> 4);
            if (chunkAtCell != nullptr)
            {
                int32_t num = TerrainChunk::CalculateCellIndex(x & 0xF, 0, z & 0xF);
                while (y < 255)
                {
                    int32_t num2 = Terrain::ExtractContents(chunkAtCell->getCellValueFast(num + y + 1));

                    FluidBlock *fluidBlock = GameSingleton::blocksManager->blocks[num2]->asBlock<FluidBlock *>();
                    if (fluidBlock == nullptr)
                    {
                        int32_t cellValueFast = chunkAtCell->getCellValueFast(num + y);
                        int32_t num3 = Terrain::ExtractContents(cellValueFast);

                        FluidBlock *fluidBlock2 = GameSingleton::blocksManager->blocks[num3]->asBlock<FluidBlock *>();

                        if (fluidBlock2 != nullptr)
                        {
                            outSurfaceFluidBlock = fluidBlock2;
                            int32_t level = FluidBlock::GetLevel(Terrain::ExtractData(cellValueFast));
                            return std::make_shared<float>(y + outSurfaceFluidBlock->getLevelHeight(level));
                        }
                        outSurfaceFluidBlock = nullptr;
                        return nullptr;
                    }
                    y++;
                }
            }
        }
        outSurfaceFluidBlock = nullptr;
        return nullptr;
    }

    std::shared_ptr<float> SubsystemFluidBlockBehavior::getSurfaceHeight(int32_t x, int32_t y, int32_t z)
    {
        FluidBlock *block;
        return getSurfaceHeight(x, y, z, block);
    }

    std::shared_ptr<Vector2> SubsystemFluidBlockBehavior::calculateFlowSpeed(int32_t x, int32_t y, int32_t z, FluidBlock *&surfaceBlock, std::shared_ptr<float> &surfaceHeight)
    {
        surfaceHeight = getSurfaceHeight(x, y, z, surfaceBlock);
        if (surfaceHeight != nullptr)
        {
            y = (int32_t)*surfaceHeight;
            int32_t cellValue = subsystemTerrain->getCellValue(x, y, z);
            int32_t num = Terrain::ExtractContents(cellValue);
            if (GameSingleton::blocksManager->blocks[num]->isBlock<FluidBlock *>() == true)
            {
                int32_t cellValue2 = subsystemTerrain->getCellValue(x - 1, y, z);
                int32_t cellValue3 = subsystemTerrain->getCellValue(x + 1, y, z);
                int32_t cellValue4 = subsystemTerrain->getCellValue(x, y, z - 1);
                int32_t cellValue5 = subsystemTerrain->getCellValue(x, y, z + 1);
                int32_t num2 = Terrain::ExtractContents(cellValue2);
                int32_t num3 = Terrain::ExtractContents(cellValue3);
                int32_t num4 = Terrain::ExtractContents(cellValue4);
                int32_t num5 = Terrain::ExtractContents(cellValue5);
                int32_t level = FluidBlock::GetLevel(Terrain::ExtractData(cellValue));
                int32_t num6 = (num2 == num) ? FluidBlock::GetLevel(Terrain::ExtractData(cellValue2)) : level;
                int32_t num7 = (num3 == num) ? FluidBlock::GetLevel(Terrain::ExtractData(cellValue3)) : level;
                int32_t num8 = (num4 == num) ? FluidBlock::GetLevel(Terrain::ExtractData(cellValue4)) : level;
                int32_t num9 = (num5 == num) ? FluidBlock::GetLevel(Terrain::ExtractData(cellValue5)) : level;
                Vector2 vector;
                vector.posX = MathUtils::Sign<float>(level - num6) - MathUtils::Sign<float>(level - num7);
                vector.posY = MathUtils::Sign<float>(level - num8) - MathUtils::Sign<float>(level - num9);
                Vector2 v = vector;
                if (v.lengthSquared() > 1.0f)
                {
                    v = Vector2::Normalize(v);
                }
                // Vector2 value;
                auto it = m_fluidRandomFlowDirections.find(Point3(x, y, z));
                if (it != m_fluidRandomFlowDirections.end())
                {

                    // value.X = 0.05f * (2f * SimplexNoise.OctavedNoise(x + 0.2f * (float)SubsystemTime.GameTime, z, 0.1f, 1, 1f, 1f) - 1f);
                    // value.Y = 0.05f * (2f * SimplexNoise.OctavedNoise(x + 0.2f * (float)SubsystemTime.GameTime + 100f, z, 0.1f, 1, 1f, 1f) - 1f);

                    it->second.posX = 0.05f;
                    it->second.posY = 0.05f;
                    if (m_fluidRandomFlowDirections.size() < 1000)
                    {
                        m_fluidRandomFlowDirections.emplace(Point3(x, y, z), it->second);
                        // m_fluidRandomFlowDirections[Point3(x, y, z)] = value;
                    }
                    else
                    {
                        m_fluidRandomFlowDirections.clear();
                    }
                    v += it->second;
                }
                return std::make_shared<Vector2>(v * 2.0f);
            }
        }
        return nullptr;
    }

    std::shared_ptr<Vector2> SubsystemFluidBlockBehavior::calculateFlowSpeed(int32_t x, int32_t y, int32_t z)
    {
        FluidBlock *block;
        std::shared_ptr<float> surface = std::make_shared<float>();
        return calculateFlowSpeed(x, y, z, block, surface);
    }

    void SubsystemFluidBlockBehavior::spreadFluid()
    {
        static Point2 sideNeighbors[4]{
            Point2(-1, 0),
            Point2(1, 0),
            Point2(0, -1),
            Point2(0, 1)};
        for (int32_t i = 0; i < 2; i++)
        {
            // for(auto it = m_toUpdate.begin(); it != m_toUpdate.end(); ++it)

            for (const auto &pair : m_toUpdate)
            {
                int32_t x = pair.posX;
                int32_t y = pair.posY;
                int32_t z = pair.posZ;
                int32_t cellValue = subsystemTerrain->getCellValue(x, y, z);
                int32_t contents = Terrain::ExtractContents(cellValue);
                int32_t data = Terrain::ExtractData(cellValue);
                int32_t level = FluidBlock::GetLevel(data);
                if (isTheSameFluid(contents))
                {
                    int32_t cellValue2 = subsystemTerrain->getCellValue(x, y - 1, z);
                    int32_t contents2 = Terrain::ExtractContents(cellValue2);
                    int32_t data2 = Terrain::ExtractData(cellValue2);
                    int32_t level2 = FluidBlock::GetLevel(data2);
                    int32_t num = m_fluidBlock->getMaxFluidLevel() + 1;
                    int32_t num2 = 0;
                    for (int32_t j = 0; j < 4; j++)
                    {
                        int32_t cellValue3 = subsystemTerrain->getCellValue(x + sideNeighbors[j].posX, y, z + sideNeighbors[j].posY);
                        int32_t contents3 = Terrain::ExtractContents(cellValue3);
                        if (isTheSameFluid(contents3))
                        {
                            int32_t level3 = FluidBlock::GetLevel(Terrain::ExtractData(cellValue3));
                            num = MathUtils::Min<int32_t>(num, level3);
                            if (level3 == 0)
                            {
                                num2++;
                            }
                        }
                    }
                    if (level != 0 && level <= num)
                    {
                        int32_t contents4 = Terrain::ExtractContents(subsystemTerrain->getCellValue(x, y + 1, z));
                        if (isTheSameFluid(contents4) == false)
                        {
                            if (num + 1 > m_fluidBlock->getMaxFluidLevel())
                            {
                                set(x, y, z, 0);
                            }
                            else
                            {
                                set(x, y, z, Terrain::MakeBlockValue(contents, 0, FluidBlock::SetLevel(data, num + 1)));
                            }
                            continue;
                        }
                    }
                    if (m_generateSources && level != 0 && num2 >= 2)
                    {
                        set(x, y, z, Terrain::MakeBlockValue(contents, 0, FluidBlock::SetLevel(data, 0)));
                    }
                    else if (isTheSameFluid(contents2))
                    {
                        if (level2 > 1)
                        {
                            set(x, y - 1, z, Terrain::MakeBlockValue(contents2, 0, FluidBlock::SetLevel(data2, 1)));
                        }
                    }
                    else if (false == onFluidInteract(cellValue2, x, y - 1, z, Terrain::MakeBlockValue(m_fluidBlock->getIndex(), 0, FluidBlock::SetLevel(0, 1))) && level < m_fluidBlock->getMaxFluidLevel())
                    {
                        m_visited.clear();
                        int32_t num3 = levelAtNearestFall(x + 1, y, z, level + 1, m_visited);
                        int32_t num4 = levelAtNearestFall(x - 1, y, z, level + 1, m_visited);
                        int32_t num5 = levelAtNearestFall(x, y, z + 1, level + 1, m_visited);
                        int32_t num6 = levelAtNearestFall(x, y, z - 1, level + 1, m_visited);
                        int32_t num7 = MathUtils::Min<int32_t>(num3, num4, num5, num6);
                        if (num3 == num7)
                        {
                            flowTo(x + 1, y, z, level + 1);
                            flowTo(x, y, z - 1, m_fluidBlock->getMaxFluidLevel());
                            flowTo(x, y, z + 1, m_fluidBlock->getMaxFluidLevel());
                        }
                        if (num4 == num7)
                        {
                            flowTo(x - 1, y, z, level + 1);
                            flowTo(x, y, z - 1, m_fluidBlock->getMaxFluidLevel());
                            flowTo(x, y, z + 1, m_fluidBlock->getMaxFluidLevel());
                        }
                        if (num5 == num7)
                        {
                            flowTo(x, y, z + 1, level + 1);
                            flowTo(x - 1, y, z, m_fluidBlock->getMaxFluidLevel());
                            flowTo(x + 1, y, z, m_fluidBlock->getMaxFluidLevel());
                        }
                        if (num6 == num7)
                        {
                            flowTo(x, y, z - 1, level + 1);
                            flowTo(x - 1, y, z, m_fluidBlock->getMaxFluidLevel());
                            flowTo(x + 1, y, z, m_fluidBlock->getMaxFluidLevel());
                        }
                    }
                }
            }
            m_toUpdate.clear();
            for (auto it = m_toSet.begin(); it != m_toSet.end(); ++it)
            {
                int32_t x2 = it->first.posX;
                int32_t y2 = it->first.posY;
                int32_t z2 = it->first.posZ;
                int32_t value = it->second;
                int32_t contents5 = Terrain::ExtractContents(it->second);
                int32_t cellContents = subsystemTerrain->getCellContents(x2, y2, z2);
                FluidBlock *fluidBlock = GameSingleton::blocksManager->blocks[cellContents]->asBlock<FluidBlock *>();

                if (fluidBlock != nullptr && isTheSameFluid(cellContents) == false)
                {
                    subsystemTerrain->destroyCell(0, x2, y2, z2, value, false);
                }
                else
                {
                    subsystemTerrain->changeCell(x2, y2, z2, value);
                }
            }
            m_toSet.clear();
        }
    }
    
    bool SubsystemFluidBlockBehavior::onFluidInteract(int32_t interactValue, int32_t x, int32_t y, int32_t z, int32_t fluidValue)
    {
        if (GameSingleton::blocksManager->blocks[Terrain::ExtractContents(interactValue)]->IsFluidBlocker == false)
        {
            subsystemTerrain->destroyCell(0, x, y, z, 0, false);
            set(x, y, z, fluidValue);
            return true;
        }
        return false;
    }

    void SubsystemFluidBlockBehavior::set(int32_t x, int32_t y, int32_t z, int32_t value)
    {
        Point3 key(x, y, z);
        if (m_toSet.find(key) == m_toSet.end())
        {
            m_toSet[key] = value;
        }
    }
    void SubsystemFluidBlockBehavior::flowTo(int32_t x, int32_t y, int32_t z, int32_t level)
    {
        if (level > m_fluidBlock->getMaxFluidLevel())
        {
            return;
        }
        int32_t cellValue = subsystemTerrain->getCellValue(x, y, z);
        int32_t contents = Terrain::ExtractContents(cellValue);
        int32_t data = Terrain::ExtractData(cellValue);

        if (isTheSameFluid(contents) == true)
        {
            int32_t level2 = FluidBlock::GetLevel(data);
            if (level < level2)
            {
                set(x, y, z, Terrain::MakeBlockValue(contents, 0, FluidBlock::SetLevel(data, level)));
            }
        }
        else
        {
            onFluidInteract(cellValue, x, y, z, Terrain::MakeBlockValue(m_fluidBlock->getIndex(), 0, FluidBlock::SetLevel(0, level)));
        }
    }

    int32_t SubsystemFluidBlockBehavior::levelAtNearestFall(int32_t x, int32_t y, int32_t z, int32_t level, std::map<Point3, int32_t> &levels)
    {
        if (level > m_fluidBlock->getMaxFluidLevel())
        {
            return INT32_MAX;
        }
        auto it = levels.find(Point3(x, y, z));
        int32_t value;
        if (it == levels.end())
        {
            value = INT32_MAX;
        }
        else
        {
            value = it->second;
        }
        if (level >= value)
        {
            return INT32_MAX;
        }
        levels[Point3(x, y, z)] = level;
        int32_t cellValue = subsystemTerrain->getCellValue(x, y, z);
        int32_t num = Terrain::ExtractContents(cellValue);
        if (isTheSameFluid(num))
        {
            if (FluidBlock::GetLevel(Terrain::ExtractData(cellValue)) < level)
            {
                return INT32_MAX;
            }
        }
        else if (GameSingleton::blocksManager->blocks[num]->IsFluidBlocker)
        {
            return INT32_MAX;
        }
        int32_t num2 = Terrain::ExtractContents(subsystemTerrain->getCellValue(x, y - 1, z));
        Block *block = GameSingleton::blocksManager->blocks[num2];
        if (isTheSameFluid(num2) || false == block->IsFluidBlocker)
        {
            return level;
        }
        int32_t x2 = levelAtNearestFall(x - 1, y, z, level + 1, levels);
        int32_t x3 = levelAtNearestFall(x + 1, y, z, level + 1, levels);
        int32_t x4 = levelAtNearestFall(x, y, z - 1, level + 1, levels);
        int32_t x5 = levelAtNearestFall(x, y, z + 1, level + 1, levels);
        return MathUtils::Min<int32_t>(x2, x3, x4, x5);
    }

    void SubsystemFluidBlockBehavior::load(const nlohmann::json &json)
    {
        subsystemTerrain = GameSingleton::singletonPtr->getSingleton<SubsystemTerrain>();
    }
}